﻿//===============================================================================
// Microsoft patterns & practices Enterprise Library Contribution
// Common Core Extensions
//===============================================================================

using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Windows.Forms;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Practices.EnterpriseLibrary.Common.Configuration;
using Microsoft.Practices.EnterpriseLibrary.Configuration.Design;
using EntLibContrib.Common.Properties;

namespace EntLibContrib.Common.Configuration.Design.Editors
{
	/// <summary>
	/// Handles the building of a tree view based on assemblies, filtered types and public methods.
	/// </summary>
	internal class MethodSelector
	{
		#region Fields
		private TreeNode rootNode;
		private TypeMethodName currentTypeMethodName;
		private BaseTypeAttribute baseTypeAttribute;
		private MethodFilterAttribute methodFilterAttribute;
		private TreeNode currentMethodTreeNode;
		private TreeView treeView;
		private Hashtable loadedAssemblies;
		#endregion

		#region Construction
		/// <summary>
		/// Initialize a new instance of the <see cref="MethodSelector"/> class with the current type method name,
		/// a base type attribute for filtering and a <see cref="TreeView"/> to display the types and methods.
		/// </summary>
		/// <param name="currentTypeMethodName">The current type method name.</param>
		/// <param name="baseTypeAttribute">The base type attribute.</param>
		/// <param name="methodFilterAttribute">The method filter attribute.</param>
		/// <param name="treeView">A <see cref="TreeView"/> to display the types.</param>
		public MethodSelector(TypeMethodName currentTypeMethodName, BaseTypeAttribute baseTypeAttribute, MethodFilterAttribute methodFilterAttribute, TreeView treeView)
		{
			if (baseTypeAttribute == null)
				throw new ArgumentNullException("baseTypeAttribute");

			if (methodFilterAttribute == null)
				throw new ArgumentNullException("methodFilterAttribute");

			if (treeView == null)
				throw new ArgumentNullException("treeView");

			loadedAssemblies = new Hashtable();
			this.currentTypeMethodName = currentTypeMethodName;
			this.baseTypeAttribute = baseTypeAttribute;
			this.methodFilterAttribute = methodFilterAttribute;
			this.treeView = treeView;

			LoadMethods();
		}
		#endregion

		#region Public Methods
		/// <summary>
		/// Loads the tree based on an assembly.
		/// </summary>
		/// <param name="assembly">The assembly to use to load the tree.</param>
		/// <returns>true if the tree contains the assembly; otherwise false.</returns>
		public TreeNode LoadTreeView(Assembly assembly)
		{
			if (loadedAssemblies.Contains(assembly.FullName))
			{
				foreach (TreeNode assemblyTreeNode in rootNode.Nodes)
				{
					if (assemblyTreeNode.Text == assembly.GetName().Name)
						return assemblyTreeNode;
				}
			}

			TreeNodeTable nodeTable = new TreeNodeTable(assembly, methodFilterAttribute);
			Type[] types = LoadTypesFromAssembly(assembly);
			if (types == null)
				return null;

			LoadValidTypeNodes(types, nodeTable);

			return AddTypesToTreeView(assembly, nodeTable);
		}

		/// <summary>
		/// Expands all assembly treeview nodes in the UI.
		/// </summary>
		public void ExpandAssemblyNodes()
		{
			foreach (TreeNode assemblyNode in this.rootNode.Nodes)
			{
				assemblyNode.ExpandAll();
			}
		}

		/// <summary>
		/// Collapses all assembly treeview nodes in the UI.
		/// </summary>
		public void CollapseAssemblyNodes()
		{
			foreach (TreeNode assemblyNode in this.rootNode.Nodes)
			{
				assemblyNode.Collapse();
			}
		}

		/// <summary>
		/// Sorts an Positions the tree view.
		/// </summary>
		/// <param name="selectedNode">The selected node.</param>
		public void PositionTreeView(TreeNode selectedNode)
		{
			treeView.Sort();
			this.rootNode.Expand();
			CollapseAssemblyNodes();
			if (selectedNode == null)
				treeView.SelectedNode = rootNode;
			else
				treeView.SelectedNode = selectedNode;
			treeView.SelectedNode.ExpandAll();
			treeView.SelectedNode.EnsureVisible();
		}
		#endregion

		#region Private Methods
		/// <summary>
		/// Loads all valid types and methods in the current domain.
		/// </summary>
		private void LoadMethods()
		{
			// Build the assemblies root node
			this.rootNode = new TreeNode(InternalResources.AssembliesLabelText, 0, 1);
			this.treeView.Nodes.Add(rootNode);
			// load the assemblies in the current domain filtered to those that contain types that match the filter criteria
			foreach (Assembly assembly in AppDomain.CurrentDomain.GetAssemblies())
			{
				LoadTreeView(assembly);
			}

			PositionTreeView(currentMethodTreeNode);
		}

		/// <summary>
		/// Loads the valid type nodes.
		/// </summary>
		/// <param name="types">The types.</param>
		/// <param name="nodeTable">The node table.</param>
		private void LoadValidTypeNodes(IEnumerable<Type> types, TreeNodeTable nodeTable)
		{
			if (types == null)
				return;

			foreach (Type type in types)
			{
				if (IsTypeValid(type))
				{
					TreeNode newNode = nodeTable.AddType(type);
					if (newNode != null)
					{
						foreach (TreeNode methodNode in newNode.Nodes)
						{
							if ((type.AssemblyQualifiedName == currentTypeMethodName.TypeName) && (methodNode.Text == currentTypeMethodName.MethodName))
								currentMethodTreeNode = methodNode;
						}
					}
				}
			}
		}

		/// <summary>
		/// Adds the types to the tree view.
		/// </summary>
		/// <param name="assembly">The assembly.</param>
		/// <param name="nodeTable">The node table.</param>
		/// <returns>
		/// The assembly TreeNode on successfully adding an assembly tree node, otherwise null
		/// </returns>
		private TreeNode AddTypesToTreeView(Assembly assembly, TreeNodeTable nodeTable)
		{
			if (nodeTable.AssemblyNode.Nodes.Count > 0)
			{
				rootNode.Nodes.Add(nodeTable.AssemblyNode);
				loadedAssemblies[assembly.FullName] = 1;
				return nodeTable.AssemblyNode;
			}

			return null;
		}

		/// <summary>
		/// Determines if a type is valid.
		/// </summary>
		/// <param name="type">The type to be checked.</param>
		/// <returns>
		/// true if the type is valid; otherwise, false.
		/// </returns>
		private bool IsTypeValid(Type type)
		{

			if (!IsSet(TypeSelectorIncludes.Interfaces) && type.IsInterface)
				return false;

			if (!IsSet(TypeSelectorIncludes.BaseType) && (type == baseTypeAttribute.BaseType))
				return false;

			if (!IsSet(TypeSelectorIncludes.AbstractTypes) && (type.IsAbstract || type.IsInterface))
				return false;

			if (!IsSet(TypeSelectorIncludes.NonpublicTypes) && !(type.IsPublic || type.IsNestedPublic))
				return false;

			if (!baseTypeAttribute.BaseType.IsAssignableFrom(type))
				return false;

			if (baseTypeAttribute.ConfigurationType != null)
			{
				object[] configurationElementTypeAttributes = type.GetCustomAttributes(typeof(ConfigurationElementTypeAttribute), true);
				if (configurationElementTypeAttributes.Length == 0)
					return false;

				ConfigurationElementTypeAttribute configElementTypeAttribute = (ConfigurationElementTypeAttribute)configurationElementTypeAttributes[0];
				if (baseTypeAttribute.ConfigurationType != configElementTypeAttribute.ConfigurationType)
					return false;
			}

			return true;
		}

		/// <summary>
		/// Determines whether the specified compare flag is set.
		/// </summary>
		/// <param name="compareFlag">The compare flag.</param>
		/// <returns>
		/// 	<c>true</c> if the specified compare flag is set; otherwise, <c>false</c>.
		/// </returns>
		private bool IsSet(TypeSelectorIncludes compareFlag)
		{
			return ((baseTypeAttribute.TypeSelectorIncludes & compareFlag) == compareFlag);
		}
		#endregion

		#region Private Static Methods
		/// <summary>
		/// Loads the types from an assembly.
		/// </summary>
		/// <param name="assembly">The assembly.</param>
		/// <returns>List of types</returns>
		[SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes")]
		private static Type[] LoadTypesFromAssembly(Assembly assembly)
		{
			Type[] types = null;
			try
			{
				types = assembly.GetTypes();
			}
			catch
			{
				// this is here to ignore the ReflectionTypeLoadException where a
				// type in the assembly is dependant on a type in another assembly
				// that has not been loaded and cannot be loaded at the time the
				// GetTypes() method call is made.
			}

			return types;
		}
		#endregion

		#region Sub Classes
		/// <devdoc>
		/// Represents the table of tree nodes by assembly.
		/// </devdoc>
		private class TreeNodeTable
		{
			#region Fields
			private TreeNode assemblyNode;
			private Hashtable namespaceTable;
			private MethodFilterAttribute methodFilterAttribute;
			#endregion

			#region Properties
			/// <summary>
			/// Gets the assembly node.
			/// </summary>
			/// <value>The assembly node.</value>
			public TreeNode AssemblyNode
			{
				get { return assemblyNode; }
			}
			#endregion

			#region Construction
			/// <summary>
			/// Initializes a new instance of the <see cref="TreeNodeTable"/> class.
			/// </summary>
			/// <param name="assembly">The assembly.</param>
			/// <param name="methodFilterAttribute">The method filter attribute.</param>
			public TreeNodeTable(Assembly assembly, MethodFilterAttribute methodFilterAttribute)
			{
				this.namespaceTable = new Hashtable();
				this.assemblyNode = new TreeNode(assembly.GetName().Name, (int)MethodImages.Assembly, (int)MethodImages.Assembly);
				this.methodFilterAttribute = methodFilterAttribute;
			}
			#endregion

			#region Public Methods
			/// <summary>
			/// Adds the type.
			/// </summary>
			/// <param name="type">The type.</param>
			/// <returns>TreeNode representing a type</returns>
			public TreeNode AddType(Type type)
			{
				// create the type tree node
				TreeNode typeNode = new TreeNode(type.Name, (int)MethodImages.Class, (int)MethodImages.Class);
				// get the public methods for this type
				List<string> methods = LoadMethodsFromType(type);
				// if there are no public methods then reject this type
				if (methods.Count == 0)
					return null;
				// add the method nodes
				foreach (string method in methods)
				{
					TreeNode methodNode = AddMethod(type, method);
					typeNode.Nodes.Add(methodNode);
				}
				// find the namespace node for the type
				TreeNode namespaceNode = GetNamespaceNode(type);
				// add the type node to the associated namespace node
				namespaceNode.Nodes.Add(typeNode);
				// return the new type node
				return typeNode;
			}
			#endregion

			#region Private Methods
			/// <summary>
			/// Gets the namespace node associated with a type.
			/// </summary>
			/// <param name="type">The type.</param>
			/// <returns>a namespace tree node</returns>
			/// <remarks>If the type has no namespace then it is simply added to the assembly node</remarks>
			private TreeNode GetNamespaceNode(Type type)
			{
				TreeNode namespaceNode;
				if (type.Namespace == null)
					namespaceNode = assemblyNode;
				else if (namespaceTable.ContainsKey(type.Namespace))
					namespaceNode = (TreeNode)namespaceTable[type.Namespace];
				else
				{
					namespaceNode = new TreeNode(type.Namespace, (int)MethodImages.Namespace, (int)MethodImages.Namespace);
					assemblyNode.Nodes.Add(namespaceNode);
					namespaceTable.Add(type.Namespace, namespaceNode);
				}
				return namespaceNode;
			}

			/// <summary>
			/// Loads the type of the methods from.
			/// </summary>
			/// <param name="type">The type.</param>
			/// <returns></returns>
			private List<string> LoadMethodsFromType(Type type)
			{
				// we are only interested in public instance methods declared in the given type
				MethodInfo[] methods = type.GetMethods(methodFilterAttribute.BindingFlags);
				// now we strip off all methods with special names like get and set accessors for properties
				List<string> methodNames = new List<string>();
				foreach (MethodInfo method in methods)
				{
					if (!method.IsSpecialName)
						methodNames.Add(method.Name);
				}
				return methodNames;
			}
			#endregion

			#region Private Static Methods
			/// <summary>
			/// Adds the method.
			/// </summary>
			/// <param name="type">The type.</param>
			/// <param name="method">The method information.</param>
			/// <returns>a TreeNode representing a method</returns>
			private static TreeNode AddMethod(Type type, string method)
			{
				TreeNode methodNode = new TreeNode(method, (int)MethodImages.Method, (int)MethodImages.Method);
				methodNode.Tag = new TypeMethodName(type.AssemblyQualifiedName, method);
				return methodNode;
			}
			#endregion
		}
		#endregion
	}
}
